/*
 * Copyright 2011 Raimon Rodriguez
 * 
 * This file is part of sgfx.
 * 
 * sgfx is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * sgfx is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with sgfx. If not, see <http://www.gnu.org/licenses/>.
 */

package com.googlecode.sgfx.base;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.awt.AWTKeyStroke;
import java.awt.Component;
import java.awt.Container;
import java.awt.FocusTraversalPolicy;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.SortingFocusTraversalPolicy;

/**
 * <p>
 * Utility class to help deal with focus traversal matters. It reduces boilerplate code when setting
 * or getting focus traversal keys for <code>KeyboardFocusManager</code> and <code>Component</code>.
 * </p>
 * <p>
 * Please see <a href="http://download.oracle.com/javase/tutorial/uiswing/misc/focus.html">
 * How to Use the Focus Subsystem</a>, a section in The Java Tutorial, and the
 * <a href="http://download.oracle.com/javase/7/docs/api/java/awt/doc-files/FocusSpec.html">
 * Focus Specification</a> for more information.
 * </p>
 *
 * @author raimon.rodriguez
 */
public final class FocusTraversal {
    private static final Log log = Log.getLog(FocusTraversal.class);

    
    private FocusTraversal() {}


    /**
     * Returns {@code true} if the component is enabled, visible and focusable. Otherwise returns
     * {@code false}.
     *
     * For more information, see
     * <a href="http://download.oracle.com/javase/tutorial/uiswing/misc/focus.html#focusable">
     *     Making a Custom Component Focusable
     * </a>, a subsection in How to Use the Focus Subsystem.
     *
     * @param pComponent the component to be checked for focus eligibility.
     * @return {@code true} if the component is enabled, visible and focusable. Otherwise returns
     * {@code false}.
     * @throws NullPointerException if {@code pComponent} is null.
     * @see SortingFocusTraversalPolicy#accept(java.awt.Component).
     */
    public static boolean isFocusEligible(JComponent pComponent) {
        return pComponent.isEnabled() && pComponent.isVisible() && pComponent.isFocusable();
    }

    /**
     * Creates a new custom {@code FocusTraversalPolicy} that traverses the specified components in
     * the order provided in the parameters list. At least two components must be provided. The first
     * component is also the default one. Disabled, invisible and/or unfocusable components will be
     * skipped from the focus traversal cycle.
     *
     * After calling this method, all provided components are guaranteed to be focusable, i.e. a
     * call {@code isFocusable()} will return {@code true}.
     *
     * Repeated components will be silently discarded.
     *
     * @param pFirst the first and default component in the focus traversal cycle.
     * @param pSecond the second component in the focus traversal cycle.
     * @param pRest the rest of the components in the focus traversal cycle.
     * @return a new custom {@code FocusTraversalPolicy} that traverses the specified components in
     * the same order as the parameters list.
     * @throws NullPointerException if any of the components is {@code null}.
     * @throws IllegalArgumentException if there are less than two unique component instances.
     * @see FocusTraversalPolicy
     * @see Container#setFocusTraversalPolicy(java.awt.FocusTraversalPolicy)
     */
    public static FocusTraversalPolicy newPolicy(JComponent pFirst, JComponent pSecond,
            JComponent... pRest) {
        Set<JComponent> tComponentsSet = ImmutableSet.<JComponent>builder().add(pFirst, pSecond)
                .add(pRest).build();

        Preconditions.checkArgument(tComponentsSet.size() >= 2, "less than two unique components");

        List<JComponent> tComponents = new ArrayList<JComponent>(tComponentsSet.size());
        for (JComponent component : tComponentsSet) {
            component.setFocusable(true);
            tComponents.add(component);
        }

        return new CustomFocusTraversalPolicy(tComponents);
    }

    /**
     * Sets the specified key strokes as the default forward traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. If no key stroke is specified 
     * or an empty array is provided, the default forward traversal keys are disabled completely.
     * Duplicate key strokes will be silently discarded.
     *
     * @param pKeys the key strokes to be set as default forward traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if a {@code null} array is passed or if any of the elements is
     * {@code null}.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyStroke
     */
    public static void setDefaultForwardKeys(KeyStroke... pKeys) {
        setDefaultForwardKeys(ImmutableSet.copyOf(pKeys));
    }

    /**
     * Sets the specified key stroke set as the default forward traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. An empty set disables
     * the default forward traversal keys completely.
     *
     * @param pKeys the key strokes to be set as default forward traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if {@code pKeys} is {@code null} or it contains any {@code null}
     * elements.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyEvent
     */
    public static void setDefaultForwardKeys(Set<? extends AWTKeyStroke> pKeys) {
        setDefaultKeys(pKeys, KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
    }

    /**
     * Returns a <i>modifiable</i> copy of the default forward traversal keys of the current 
     * {@code KeyboardFocusManager}. Modifications to this set won't change the original default
     * traversal keys.
     *
     * <p>
     * This method call effectively returns a copy of the set returned by:
     * </p>
     * <code>
     * KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
     * </code>
     *
     * @return a modifiable copy of the default forward traversal keys of the current
     * {@code KeyboardFocusManager}.
     * @see KeyboardFocusManager#getDefaultFocusTraversalKeys(int)
     */
    public static Set<AWTKeyStroke> getDefaultForwardKeys() {
        return getDefaultKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
    }

    /**
     * Sets the specified key strokes as the default backward traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. If no key stroke is specified
     * or an empty array is provided, the default backward traversal keys are disabled completely.
     * Duplicate key strokes will be silently discarded.
     *
     * @param pKeys the list of key strokes to be set as default backward traversal keys.
     * @throws NullPointerException if a {@code null} array is passed or if any of the elements is
     * {@code null}.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyStroke
     */
    public static void setDefaultBackwardKeys(KeyStroke... pKeys) {
        setDefaultBackwardKeys(ImmutableSet.copyOf(pKeys));
    }

    /**
     * Sets the specified key stroke set as the default backward traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. An empty set disables the
     * default backward traversal keys completely.
     *
     * @param pKeys the set of key strokes to be set as default backward traversal keys.
     * @throws NullPointerException if {@code pKeys} is {@code null} or it contains any {@code null}
     * elements.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyEvent
     */
    public static void setDefaultBackwardKeys(Set<? extends AWTKeyStroke> pKeys) {
        setDefaultKeys(pKeys, KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
    }

    /**
     * Returns a <i>modifiable</i> copy of the default backward traversal keys of the current
     * {@code KeyboardFocusManager}. Modifications to this set won't change the original default
     * traversal keys.
     * <p>
     * This method call effectively returns a copy of the set returned by:
     * </p>
     * <code>
     * KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
     * </code>
     *
     * @return a modifiable copy of the default backward traversal keys of the current
     * {@code KeyboardFocusManager}.
     * @see KeyboardFocusManager#getDefaultFocusTraversalKeys(int)
     */
    public static Set<AWTKeyStroke> getDefaultBackwardKeys() {
        return getDefaultKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
    }

    /**
     * Sets the specified key strokes as the default up-cycle traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. If no key stroke is specified
     * or an empty array is provided, the default up-cycle traversal keys are disabled completely.
     * Duplicate key strokes will be silently discarded.
     *
     * @param pKeys the key strokes to be set as default up-cycle traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if a {@code null} array is passed or if any of the elements is
     * {@code null}.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyStroke
     */
    public static void setDefaultUpCycleKeys(KeyStroke... pKeys) {
        setDefaultUpCycleKeys(ImmutableSet.copyOf(pKeys));
    }

    /**
     * Sets the specified key stroke set as the default up-cycle traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. An empty set disables
     * the default up-cycle traversal keys completely.
     *
     * @param pKeys the key strokes to be set as default up-cycle traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if {@code pKeys} is {@code null} or it contains any {@code null}
     * elements.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyEvent
     */
    public static void setDefaultUpCycleKeys(Set<? extends AWTKeyStroke> pKeys) {
        setDefaultKeys(pKeys, KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS);
    }

    /**
     * Returns a <i>modifiable</i> copy of the default up-cycle traversal keys of the current
     * {@code KeyboardFocusManager}. Modifications to this set won't change the original default
     * traversal keys.
     *
     * <p>
     * This method call effectively returns a copy of the set returned by:
     * </p>
     * <code>
     * KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS)
     * </code>
     *
     * @return a modifiable copy of the default up-cycle traversal keys of the current
     * {@code KeyboardFocusManager}.
     * @see KeyboardFocusManager#getDefaultFocusTraversalKeys(int)
     */
    public static Set<AWTKeyStroke> getDefaultUpCycleKeys() {
        return getDefaultKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS);
    }

    /**
     * Sets the specified key strokes as the default down-cycle traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. If no key stroke is specified
     * or an empty array is provided, the default down-cycle traversal keys are disabled completely.
     * Duplicate key strokes will be silently discarded.
     *
     * @param pKeys the key strokes to be set as default down-cycle traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if a {@code null} array is passed or if any of the elements is
     * {@code null}.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyStroke
     */
    public static void setDefaultDownCycleKeys(KeyStroke... pKeys) {
        setDefaultDownCycleKeys(ImmutableSet.copyOf(pKeys));
    }

    /**
     * Sets the specified key stroke set as the default down-cycle traversal keys for the current
     * {@code KeyboardFocusManager}, clearing any previously set keys. An empty set disables
     * the default down-cycle traversal keys completely.
     *
     * @param pKeys the key strokes to be set as default down-cycle traversal keys for the current
     * {@code KeyboardFocusManager}.
     * @throws NullPointerException if {@code pKeys} is {@code null} or it contains any {@code null}
     * elements.
     * @see KeyboardFocusManager#setDefaultFocusTraversalKeys(int, java.util.Set)
     * @see KeyEvent
     */
    public static void setDefaultDownCycleKeys(Set<? extends AWTKeyStroke> pKeys) {
        setDefaultKeys(pKeys, KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS);
    }

    /**
     * Returns a <i>modifiable</i> copy of the default down-cycle traversal keys of the current
     * {@code KeyboardFocusManager}. Modifications to this set won't change the original default
     * traversal keys.
     *
     * <p>
     * This method call effectively returns a copy of the set returned by:
     * </p>
     * <code>
     * KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS)
     * </code>
     *
     * @return a modifiable copy of the default down-cycle traversal keys of the current
     * {@code KeyboardFocusManager}.
     * @see KeyboardFocusManager#getDefaultFocusTraversalKeys(int)
     */
    public static Set<AWTKeyStroke> getDefaultDownCycleKeys() {
        return getDefaultKeys(KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS);
    }


    private static void setDefaultKeys(Set<? extends AWTKeyStroke> pKeys, int pKeysId) {
        getCurrentKeyboardFocusManager().setDefaultFocusTraversalKeys(
                pKeysId, pKeys instanceof ImmutableSet? pKeys : ImmutableSet.copyOf(pKeys));
    }

    private static Set<AWTKeyStroke> getDefaultKeys(int pKeysId) {
        return Sets.newHashSet(getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(
                pKeysId));
    }

    private static KeyboardFocusManager getCurrentKeyboardFocusManager() {
        return KeyboardFocusManager.getCurrentKeyboardFocusManager();
    }
    
    private static class CustomFocusTraversalPolicy extends FocusTraversalPolicy {
        // Requirements for the collection: no repeats, no nulls, respects insertion order, get first
        // get last, get next component
        private List<JComponent> components;

        private CustomFocusTraversalPolicy(List<JComponent> pComponents) {
            components = pComponents;
        }

        public Component getComponentAfter(Container pContainer, Component pComponent) {
            log.finest("pContainer=%s, pComponent=%s", pContainer, pComponent);

            int index = (components.indexOf(pComponent) + 1) % components.size();

            JComponent next = components.get(index);

            if (isFocusEligible(next)) return next;
            else return getComponentAfter(pContainer, next);
        }

        public Component getComponentBefore(Container pContainer, Component pComponent) {
            log.finest("pContainer=%s, pComponent=%s", pContainer, pComponent);

            int index = components.indexOf(pComponent) - 1;
            if (index < 0) index = components.size() - 1;

            JComponent next = components.get(index);

            if (isFocusEligible(next)) return next;
            else return getComponentBefore(pContainer, next);
        }

        public Component getFirstComponent(Container pContainer) {
            log.finest("pContainer=%s", pContainer);
            
            return components.get(0);
        }

        public Component getLastComponent(Container pContainer) {
            log.finest("pContainer=%s", pContainer);

            return components.get(components.size() - 1);
        }

        public Component getDefaultComponent(Container pContainer) {
            log.finest("pContainer=%s", pContainer);

            return getFirstComponent(pContainer);
        }
    }
}
